package mill.javalib.spring.boot

import mainargs.Flag
import mill.{T, Task}
import mill.api.{ModuleRef, PathRef}
import mill.javalib.{Dep, DepSyntax, JavaModule, NativeImageModule}

/**
 * A module that can be used to configure Spring Boot projects and provides functionality
 * for AOT processing and native GraalVM builds.
 *
 * For compatibility with initializr projects ([[https://start.spring.io/]]),
 * mix this module with the [[MavenModule]].
 */
@mill.api.experimental
trait SpringBootModule extends JavaModule {
  outer =>

  /** Spring boot version as can be found in [[https://start.spring.io/]] */
  def springBootPlatformVersion: T[String]

  /** org.springframework.boot:spring-boot-dependencies with [[springBootPlatformVersion]] as the version */
  override def bomMvnDeps: T[Seq[Dep]] = Seq(
    mvn"org.springframework.boot:spring-boot-dependencies:${springBootPlatformVersion()}"
  )

  /**
   * The Module holding the Spring Boot tools.
   */
  def springBootToolsModule: ModuleRef[SpringBootToolsModule] = ModuleRef(SpringBootToolsModule)

  /**
   * The group id to be used for SpringBoot's AOT processing. Default is empty string
   */
  def springBootGroupId: T[String] = Task {
    ""
  }

  /**
   * The artifact id to be used for Spring's AOT processing. Default is [[artifactName]]
   */
  def springBootArtifactId: T[String] = Task {
    artifactName()
  }

  /**
   * Uses the [[springBootToolsModule]] to find the SpringBootApplicationClass from the [[localRunClasspath]]
   */
  def springBootMainClass: T[String] = Task {
    mainClass()
      .toRight("No main class specified")
      .orElse(
        springBootToolsModule()
          .springBootToolsWorker()
          .findSpringBootApplicationClass(localRunClasspath().map(_.path))
      )
      .fold(l => Task.fail(l), r => r)
  }

  /**
   * Extra args passed to "org.springframework.boot.SpringApplicationAotProcessor"
   *
   * For more information go to [[https://docs.spring.io/spring-framework/reference/core/aot.html]]
   */
  def springBootProcessAOTExtraApplicationArgs: T[Seq[String]] = Task {
    Seq.empty[String]
  }

  /**
   * The runnable main class that takes care of the AOT processing.
   * Defaults to "org.springframework.boot.SpringApplicationAotProcessor"
   */
  def springBootAOTProcessorMainClass: T[String] = Task {
    "org.springframework.boot.SpringApplicationAotProcessor"
  }

  /**
   * Spring Boot AOT processing, generating "Fast classes".
   *
   * For more information go to [[https://docs.spring.io/spring-framework/reference/core/aot.html]]
   */
  def springBootProcessAOT: T[PathRef] = Task {
    val dest = Task.dest
    val applicationMainClass = springBootMainClass()

    val sourceOut = dest / "sources"
    val resourceOut = dest / "resources"
    val classOut = dest / "classes"
    val groupId = springBootGroupId()
    val artifactId = springBootArtifactId()

    val args: Array[String] = Array(
      applicationMainClass,
      sourceOut.toString,
      resourceOut.toString,
      classOut.toString,
      groupId,
      artifactId
    ) ++ springBootProcessAOTExtraApplicationArgs()

    val spawned = mill.util.Jvm.spawnProcess(
      mainClass = springBootAOTProcessorMainClass(),
      mainArgs = args,
      classPath = runClasspath().map(_.path),
      stderr = os.Inherit,
      stdout = os.Inherit
    )

    spawned.waitFor()

    PathRef(dest)
  }

  /**
   * The location of the native-image properties generated by [[springBootProcessAOT]]
   * and also derived from [[springBootGroupId]] and [[springBootArtifactId]]
   *
   * If the [[springBootGroupId]] is an empty string, group id defaults to unspecified
   * as per in [[https://github.com/spring-projects/spring-boot/blob/main/core/spring-boot/src/main/java/org/springframework/boot/SpringApplicationAotProcessor.java#L91]]
   */
  def springBootAOTNativeProperties: T[PathRef] = Task {
    val groupId = if (springBootGroupId().isEmpty)
      "unspecified"
    else
      springBootGroupId()
    val aotDir: os.Path = outer.springBootProcessAOT().path
    PathRef(
      aotDir / "resources/META-INF/native-image" / groupId / springBootArtifactId() / "native-image.properties"
    )
  }

  override def prepareOffline(all: Flag): Task.Command[Seq[PathRef]] = Task.Command {
    (
      super.prepareOffline(all)() ++
        springBootToolsModule().prepareOffline(all)()
    ).distinct
  }

  /**
   * This submodule gets the generated AOT sources, resources and compiled classes
   * and builds an optimised version of the Spring Boot Application.
   *
   * The jar build by this submodule can be started with an AOT enabled flag
   *
   * `java -Dspring.aot.enabled=true -jar assembly_jar`
   */
  trait SpringBootOptimisedBuildModule extends SpringBootModule {

    def moduleDeps = Seq(outer)

    def springBootPlatformVersion: T[String] = outer.springBootPlatformVersion()

    override def moduleDir: os.Path = outer.moduleDir

    /**
     * Enables AOT for running the application under this module
     */
    override def forkArgs = super.forkArgs() ++ Seq("-Dspring.aot.enabled=true")

    override def generatedSources: Task.Simple[Seq[PathRef]] = Task {
      val aotGeneratedSources = Seq(PathRef(outer.springBootProcessAOT().path / "sources"))
      outer.generatedSources() ++ aotGeneratedSources
    }

    override def resources: Task.Simple[Seq[PathRef]] = Task {
      val aotGeneratedResources =
        Seq(PathRef(outer.springBootProcessAOT().path / "resources"))
      outer.resources() ++ aotGeneratedResources
    }

    override def compileClasspath: Task.Simple[Seq[PathRef]] = Task {
      val aotClasses = Seq(PathRef(outer.springBootProcessAOT().path / "classes"))
      outer.compileClasspath() ++ aotClasses
    }
  }

  /**
   * This submodule gives the ability to run the Spring Boot application provided by the
   * parent module as a native GraalVM application, provided the [[outer.springBootProcessAOT]] works.
   */
  trait NativeSpringBootBuildModule extends SpringBootOptimisedBuildModule, NativeImageModule {

    /**
     * Uses the configuration path from both [[outer.springBootProcessAOT]] and
     * [[nativeMvnDepsMetadata]]
     */
    override def nativeImageOptions: Task.Simple[Seq[String]] = Task {
      val configurationsPath = outer.springBootProcessAOT().path / "resources/META-INF"
      super.nativeImageOptions() ++ Seq(
        "--configurations-path",
        configurationsPath.toString
      )
    }
  }

  trait SpringBootTestsModule extends SpringBootModule, JavaTests {
    def springBootPlatformVersion: T[String] = outer.springBootPlatformVersion()
  }
}
